﻿
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;

namespace LExperiment.NetworkLib
{
    /// <summary>
    /// wrapper for socket to communicate with its remote peer
    /// suitable for using both at server side and client side
    /// </summary>
    public sealed class LTcpChannel : IDisposable
    {
        // ===================================================== //
        #region [ member variables ]

        public event EventHandler PassiveSockClosedEvtHandler;

        private Socket m_socket;
        private IPEndPoint m_remoteEndpoint; // cache it for using after being disposed
        private NetworkStream m_stream;
        private HeaderBodyFsmParser m_parser;

        private bool m_disposed;

        #endregion

        // ===================================================== //
        #region [ constructor and destructor ]

        public LTcpChannel(Socket socket)
        {
            m_socket = socket;
            m_remoteEndpoint = (IPEndPoint)m_socket.RemoteEndPoint;
            m_stream = new NetworkStream(m_socket);

            m_parser = new HeaderBodyFsmParser(m_stream);
            m_parser.StreamEndEvtHandler += this.OnStreamEndCallback;
            m_parser.IoExceptionEvtHandler += this.OnIoExceptionCallback;
        }

        ~LTcpChannel()
        {
            Dispose(false);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private void Dispose(bool isDisposing)
        {
            if (m_disposed)
                return;

            try
            {
                if (isDisposing)
                {
                    if (m_stream != null)
                    {
                        // m_stream.Flush();// no need to call Flush, because we use this stream for reading not for writing
                        m_stream.Close();
                    }

                    if (m_socket != null)
                    {
                        // When using a connection-oriented Socket, always call the Shutdown method before closing the Socket. 
                        // This ensures that all data is sent and received on the connected socket before it is closed.
                        m_socket.Shutdown(SocketShutdown.Both);
                        m_socket.Close();
                    }
                }
            }
            finally
            {
                m_disposed = true;
            }
        }

        #endregion

        // ===================================================== //
        /// <summary>
        /// after disposed, call socket.RemoteEndpoint again will cause exception
        /// </summary>
        public IPEndPoint RemoteEndpoint { get { return m_remoteEndpoint; } }

        // ===================================================== //
        #region [ event management ]

        public event EventHandler<BodyRecvEventArgs> BodyRecvedEvtHandler
        {
            add { m_parser.BodyRecvedEvtHandler += value; }
            remove { m_parser.BodyRecvedEvtHandler -= value; }
        }

        public event EventHandler<OnlyHeaderRecvEventArgs> OnlyHeaderRecvEvtHandler
        {
            add { m_parser.OnlyHeaderRecvEvtHandler += value; }
            remove { m_parser.OnlyHeaderRecvEvtHandler -= value; }
        }

        private void OnStreamEndCallback(object source, EventArgs evtargs)
        {
            CloseLocalSocket();
        }

        private void OnIoExceptionCallback(object source, IoExceptionEventArgs evtargs)
        {
            Exception innerException = evtargs.Exception.InnerException;
            if (innerException != null && innerException is SocketException)
            {
                SocketException socketexp = (SocketException)innerException;
                if (socketexp.SocketErrorCode == SocketError.ConnectionReset)
                {
                    CloseLocalSocket();
                }
            }
            else
                throw evtargs.Exception;
        }

        #endregion

        // ===================================================== //
        #region [ IO operation ]

        public void BeginReceive()
        {
            m_parser.BeginRead();
        }

        public void BeginSend(PacketHeader header, byte[] body)
        {
            header.BodyLength = body.Length;
            byte[] wholeImage = NetworkUtility.CombineHeaderBody(header, body);

            object stateObj = wholeImage.Length; // boxing, transfer the memory from stack to heap
            m_socket.BeginSend(wholeImage, 0, wholeImage.Length, SocketFlags.None, this.OnSentCallback, stateObj);
        }

        private void OnSentCallback(IAsyncResult asyncResult)
        {
            try
            {
                int originalLength = (int)asyncResult.AsyncState;
                int sendLength = m_socket.EndSend(asyncResult);
                if (sendLength != originalLength)
                    throw new NotImplementedException("the case that whole content cannot be sent out in one write has been neglected");
            }
            catch (SocketException ex)
            {
                if (ex.SocketErrorCode == SocketError.ConnectionReset)
                    CloseLocalSocket();
                else
                    throw ex;
            }
        }

        private void CloseLocalSocket()
        {
            this.Dispose();

            if (PassiveSockClosedEvtHandler != null)
                PassiveSockClosedEvtHandler(this, EventArgs.Empty);
        }

        #endregion
    }
}